package mh.map;

import java.util.Properties;
import java.util.Vector;
import java.util.logging.Logger;

import mh.PlayerEntity;
import mh.SpellEntity;
import mh.interfaces.Constants;
import mh.map.objectset.ObjectSet;

/**
 * This class is the main class of all the tile-map system.<br>
 * It's generated from the {@link TMXMapReader} according the TMX protocol (most).
 * 
 * @author Gianmarco Laggia
 * 
 */
public class Map implements Constants {

	// =================================================== //
	//                   CONFIG ELEMENTS                   //
	// =================================================== //
	/**
	 * The Logger
	 */
	private static final Logger	LOG						= Logger.getLogger(Map.class.getName());

	/**
	 * Constant value of the orthogonal orientation.
	 */
	public static final int		ORIENTATION_ORTHOGONAL	= 1;

	// =================================================== //
	//                     TMX PROTOCOL                    //
	// =================================================== //
	/**
	 * The actual orientation of the tiles in the map.
	 */
	private int					orientation				= ORIENTATION_ORTHOGONAL;
	/**
	 * Number of vertical tiles of the map
	 */
	private final int			mapHeight;
	/**
	 * Number of horizontal tiles of the map
	 */
	private final int			mapWidth;
	/**
	 * The size of a tile (width) [NOT USED FOR NOW]
	 */
	private int					tileWidth				= TILE_SIZE;
	/**
	 * The size of a tile (height) [NOT USED FOR NOW]
	 */
	private int					tileHeight				= TILE_SIZE;
	/**
	 * The background color of the map
	 * 
	 * @deprecated
	 */
	//	private int backgroundColor;
	/**
	 * The properties of this map. It contains the map name.
	 */
	private Properties			properties;

	// ====================================================== //
	//                     IMPLEMENTATION                     //
	// ====================================================== //
	/**
	 * The name of the <code>.tmx</code> file that specifies this map.
	 */
	private String				filename;
	/**
	 * The layers of this map.
	 */
	private Vector<Layer>		layers;
	/**
	 * The tilesets used in this map.
	 */
	private Vector<TileSet>		tilesets;
	/**
	 * The tilesets used in this map.
	 */
	private Vector<ObjectSet>	objectsets;

	/**
	 * The layer with all the other entities in the map.
	 */
	private OthersLayer			othersLayer;
	/**
	 * The layer with all the other entities in the map.
	 */
	private SpellsLayer			spellsLayer;
	/**
	 * The entity of the player
	 */
	private PlayerEntity		player;
	/**
	 * Layer used to check special effects of each tile.<br>
	 * The first number is the x, the second is the y
	 */
	private int[][]				actionsLayer;

	/**
	 * Relative position of the player (x)
	 */
	private int					x;
	/**
	 * Relative position of the player (y)
	 */
	private int					y;

	// ====================================================== //
	//                        ANIMATION                       //
	// ====================================================== //
	/** The frame duration in milliseconds, i.e. how long any given frame of animation lasts */
	private final long			frameDuration			= VELOCITY;
	/**
	 * The time since the last frame change took place.<br>
	 * First value is set because we want the first (ever) move done instantly.
	 */
	private long				lastFrameChange			= this.frameDuration;

	/**
	 * Create the map that is a container for layers, objects and players. TODO THIS CAN BE REMOVED
	 * 
	 * @param mapName
	 *            The name of the map
	 * @param pl
	 *            The entity of the player
	 * @param mapw
	 *            TODO remove
	 * @param maph
	 *            TODO remove
	 * @deprecated
	 */
	@Deprecated
	public Map(String mapName, PlayerEntity pl, int mapw, int maph) {
		LOG.fine("Creating the map");
		//		this.mapName = mapName;
		this.player = pl;
		this.x = 5;
		this.y = 4;
		this.actionsLayer = new int[mapw][maph];
		// Piccolo muro verticale di 3 tile.
		this.actionsLayer[4][3] = 1; //
		this.actionsLayer[4][4] = 1; //
		this.actionsLayer[4][5] = 1; //
		// --------------------------------
		this.layers = new Vector<Layer>();
		//		this.backgroundLayer = new MapLayer(5, 4, 0, 0); //TODO CHECK
		//		this.secondLayer = new MapLayer(5, 4);
		//		this.objectsLayer = new MapLayer(5, 4);
		this.othersLayer = new OthersLayer(5, 4);
		this.spellsLayer = new SpellsLayer(5, 4);
		this.mapWidth = mapw;
		this.mapHeight = maph;
		// READ FROM FILE OR DATABASE THE DATA

	}

	/**
	 * Main constructor, creates a new map with empty vector of layers and tilesets and sets dimensions of the map (in
	 * tile units).
	 * 
	 * @param width
	 *            The width of this map
	 * @param height
	 *            The height of this map
	 * @param pl
	 *            the player entity
	 */
	public Map(int width, int height, PlayerEntity pl) {
		this.layers = new Vector<Layer>();
		this.tilesets = new Vector<TileSet>();
		this.objectsets = new Vector<ObjectSet>();
		this.mapWidth = width;
		this.mapHeight = height;
		this.player = pl;
	}

	/**
	 * Method used to draw the map and all its levels
	 * 
	 * @param delta
	 *            time passed (in ms) since the last call of the method.
	 */
	public void draw(long delta) {
		//		this.backgroundLayer.draw(delta);
		//		//		 this.secondLayer.draw(delta);
		//		//		 this.objectsLayer.draw(delta);
		//		this.player.draw(delta);
		//		this.othersLayer.draw(delta);
		//		this.spellsLayer.draw(delta);
		//		// this.foregroundLayer.draw();
		//
		//		ArrayList<SpellEntity> spells = this.spellsLayer.getSpells();
		//		ArrayList<OtherEntity> others = this.othersLayer.getOthers();
		//		for (int i = 0; i < spells.size(); i++) {
		//			for (int j = 0; j < others.size(); j++) {
		//				if (spells.get(i).collidesWith(others.get(j))) {
		//					spells.get(i).iWasHit();
		//					System.out.println("ouch, beccato");
		//				}
		//			}
		//		}
		for (int i = 0; i < this.layers.size(); i++) {
			this.layers.get(i).draw(delta);
			if (this.layers.get(i) instanceof ObjectsLayer) {
				this.player.draw(delta);
				this.layers.get(i).draw(delta);
			}
			//			if (layers.get(i) == null) {
			//				this.player.draw(delta);
			//				//DRAW PLAYER, OTHERS, and SPELLS
			//			} else {
			//			}
		}
	}

	/**
	 * This method checks if the player can move or not in a certain direction.
	 * 
	 * @param where
	 *            The destination where the player can move:<br>
	 *            - 0: down<br>
	 *            - 1: left<br>
	 *            - 2: right<br>
	 *            - 3: up<br>
	 * @return true if can move
	 */
	public boolean canMove(int where) {
		switch (where) {
			case 0:
				if (this.y < (this.mapHeight - 1)) {
					boolean isAvaible = true;
					for (Layer l : this.layers) {
						isAvaible &= l.isAvaible(this.x, this.y + 1);
					}
					return isAvaible;
					//							if (this.actionsLayer[this.x][this.y + 1] != 1) {
					//					if (this.othersLayer.isAvaible(this.x, this.y + 1)) {
					//						return true;
					//					}
					//							}
				}
				break;
			case 1:
				if (this.x > 0) {
					boolean isAvaible = true;
					for (Layer l : this.layers) {
						isAvaible &= l.isAvaible(this.x - 1, this.y);
					}
					return isAvaible;
					//							if (this.actionsLayer[this.x - 1][this.y] != 1) {
					//					if (this.othersLayer.isAvaible(this.x - 1, this.y)) {
					//						return true;
					//					}
					//							}
				}
				break;
			case 2:
				if (this.x < (this.mapWidth - 1)) {
					boolean isAvaible = true;
					for (Layer l : this.layers) {
						isAvaible &= l.isAvaible(this.x + 1, this.y);
					}
					return isAvaible;
					//							if (this.actionsLayer[this.x + 1][this.y] != 1) {

					//					if (this.othersLayer.isAvaible(this.x + 1, this.y)) {
					//						return true;
					//					}
					//							}
				}
				break;
			case 3:
				if (this.y > 0) {
					boolean isAvaible = true;
					for (Layer l : this.layers) {
						isAvaible &= l.isAvaible(this.x, this.y - 1);
					}
					return isAvaible;
					//							if (this.actionsLayer[this.x][this.y - 1] != 1) {
					//					if (this.othersLayer.isAvaible(this.x, this.y - 1)) {
					//						return true;
					//					}
					//							}
				}
				break;
		}
		return false;
	}

	/**
	 * Stops the player animation
	 */
	public void stop() {
		this.player.stop();
	}

	/**
	 * Used to move the map, if possible
	 * 
	 * @param delta
	 * @param where
	 * @return True if moved
	 */
	public boolean move(long delta, int where) {
		this.lastFrameChange += delta;
		if (this.lastFrameChange > this.frameDuration) {
			this.lastFrameChange = 0;

			if (this.canMove(where)) {
				switch (where) {
					case 0: // Down
						this.setLayersBounds(delta, 0, 1);
						break;
					case 1: // Left
						this.setLayersBounds(delta, -1, 0);
						break;
					case 2: // Right
						this.setLayersBounds(delta, 1, 0);
						// this.game.getClientTCP().send("PING:");
						break;
					case 3: // Up
						this.setLayersBounds(delta, 0, -1);
						break;
				}

				// this.game.getClientTCP().send("MOVE (" + this.backgroundLayer.getPosX() + ":" +
				////// this.backgroundLayer.getPosY() + ")");
				this.player.move(delta, where);
				return true;
			}
		}
		return false;
	}

	private void setLayersBounds(double delta, int dx, int dy) {
		this.x += dx;
		this.y += dy;
		//		this.othersLayer.setBounds(dx, dy);
		//		this.spellsLayer.setBounds(dx, dy);
		for (int i = 0; i < this.layers.size(); i++) {
			if (this.layers.get(i) == null) {

			} else {
				this.layers.get(i).setBounds(dx, dy);
			}
		}
	}

	/**
	 * Adds a spell in spell layer.
	 * 
	 * @param sprite
	 *            The path to the spell imageset
	 * @param mousex
	 *            X position of the mouse when clicked
	 * @param mousey
	 *            Y position of the mouse when clicked
	 * @return A <code>SpellEntity</code> object fitted with player position.
	 */
	public SpellEntity addSpell(String sprite, double mousex, double mousey) {
		SpellEntity s = new SpellEntity(sprite, this.x, this.y, mousex, mousey);
		this.spellsLayer.addSpell(s);
		return s;
	}

	/**
	 * TODO REMOVE
	 * 
	 * @deprecated
	 * @return nothing
	 */
	@Deprecated
	public OthersLayer getOthers() {
		return this.othersLayer;
	}

	/**
	 * Returns the properties of this map
	 * 
	 * @return A <code>Propertie</code> set.
	 */
	public Properties getProperties() {
		return this.properties;
	}

	/**
	 * Sets the orientation of the map, by now only orthogonal is possible
	 * 
	 * @param orientation
	 *            An int describing the orientation
	 */
	public void setOrientation(int orientation) {
		this.orientation = orientation;
	}

	/**
	 * Returns the actual orientation of the map
	 * 
	 * @return An integer costant of orientation
	 */
	public int getOrientation() {
		return this.orientation;
	}

	/**
	 * Set the filename of this map
	 * 
	 * @param filename
	 *            The name
	 */
	public void setFilename(String filename) {
		this.filename = filename;
	}

	/**
	 * Sets the default tile width
	 * 
	 * @param w
	 *            an int, width of a tile in pixel
	 */
	public void setTileWidth(int w) {
		this.tileWidth = w;
	}

	/**
	 * Sets the default tile height
	 * 
	 * @param h
	 *            an int, height of a tile in pixel
	 */
	public void setTileHeight(int h) {
		this.tileHeight = h;
	}

	/**
	 * Returns the width of the map in tiles unit.
	 * 
	 * @return an int.
	 */
	public int getWidth() {
		return this.mapWidth;
	}

	/**
	 * Returns the height of the map in tiles unit.
	 * 
	 * @return an int.
	 */
	public int getHeight() {
		return this.mapHeight;
	}

	/**
	 * Returns the default width of each tile.
	 * 
	 * @return An int, width of the tile in pixels.
	 */
	public int getTileWidth() {
		return this.tileWidth;
	}

	/**
	 * Returns the default height of each tile.
	 * 
	 * @return An int, height of the tile in pixels.
	 */
	public int getTileHeight() {
		return this.tileHeight;
	}

	/**
	 * Add a tileset to this map
	 * 
	 * @param tileset
	 *            the set to add
	 */
	public void addTileset(TileSet tileset) {
		if ((tileset == null) || (this.tilesets.indexOf(tileset) > -1)) {
			return;
		}
		this.tilesets.add(tileset);
	}

	/**
	 * Add an objectset to this map
	 * 
	 * @param objectset
	 *            the set to add
	 */
	public void addObjectset(ObjectSet objectset) {
		if ((objectset == null) || (this.objectsets.indexOf(objectset) > -1)) {
			return;
		}
		this.objectsets.add(objectset);
	}

	/**
	 * NOT SUPPORTED, used only to match TMX protocol.
	 * 
	 * @param bg
	 */
	public void setBackground(int bg) {
		return;
	}

	/**
	 * Adds a new layer to the map.
	 * 
	 * @param layer
	 *            The layer
	 * @return the layer added
	 */
	public Layer addLayer(Layer layer) {
		if (layer != null) {
			layer.setMap(this);
		}
		this.layers.add(layer);
		return layer;
	}

	/**
	 * Returns the number of layers of the map
	 * 
	 * @return An integer count of layers number
	 */
	public int getLayerCount() {
		return this.layers.size();
	}

	/**
	 * Returns string describing the map. The form is <code>Map[width x height
	 * x layers][tileWidth x tileHeight]</code>, for example <code>
	 * Map[64x64x2][32x32]</code>.
	 * 
	 * @return string describing map
	 */
	@Override
	public String toString() {
		return "Map[" + this.filename + "][" + this.mapWidth + "x" + this.mapHeight + "x" + (this.getLayerCount() - 1) + "][" + this.tileWidth + "x"
				+ this.tileHeight + "]";
	}

	/**
	 * @return The number of total tilesets used in this map
	 */
	public int getTilesetCount() {
		return this.tilesets.size();
	}

	/**
	 * @return The number of total objectsets used in this map
	 */
	public int getObjectsetCount() {
		return this.objectsets.size();
	}

}
